package lumis.portal.sendmail.dao.hibernate;

import java.util.Date;
import java.util.List;

import lumis.portal.PortalException;
import lumis.portal.dao.DaoException;
import lumis.portal.dao.hibernate.GenericHibernateManualUpdateDao;
import lumis.portal.dao.hibernate.ITransactionHibernate;
import lumis.portal.sendmail.IBaseMail;
import lumis.portal.sendmail.IMailBody;
import lumis.portal.sendmail.IMailSendStatus;
import lumis.portal.sendmail.IMultipleMail;
import lumis.portal.sendmail.ISingleMail;
import lumis.portal.sendmail.IMailAttachment;
import lumis.portal.sendmail.IMailConfig;
import lumis.portal.sendmail.IMailDestination;
import lumis.portal.sendmail.dao.ISendMailDao;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.util.ITransaction;
import lumis.util.PortalUtil;

import org.hibernate.Criteria;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Order;

/**
 * Hibernate based implementation of {@link ISendMailDao}.
 *  
 * @version $Revision: 8944 $ $Date: 2008-02-29 12:30:48 -0300 (Fri, 29 Feb 2008) $
 * @since 4.0.4
 */
public class SendMailDaoHib implements ISendMailDao
{
	/**
	 * Internal DAO used to persist {@link MailConfig} instances.
	 * @since 4.0.4
	 */
	private static class MailConfigDao extends GenericHibernateManualUpdateDao<MailConfig, String>
	{		
		public MailConfigDao()
		{
			super(MailConfig.class);
		}
		
		@Override
		protected String getIdentifier(MailConfig mailConfig)
		{
			return mailConfig.getId();
		}

		@Override
		public List<MailConfig> findByCriteria(ITransaction transaction, Criterion... criterions) throws DaoException
		{
			return super.findByCriteria(transaction, criterions);
		}

		@Override
		public boolean delete(String id, ITransaction transaction) throws PortalException
		{
			return super.delete(id, transaction);
		}

		@Override
		public void addOrUpdate(MailConfig bean, ITransaction transaction) throws PortalException
		{
			super.addOrUpdate(bean, transaction);
		}	
	}

	/**
	 * Internal DAO used to persist {@link Mail} instances.
	 * @since 4.0.4
	 */
	private static class MailDao extends GenericHibernateManualUpdateDao<Mail, String>
	{		
		public MailDao()
		{
			super(Mail.class);
		}
		
		@Override
		protected String getIdentifier(Mail mail)
		{
			return mail.getId();
		}

		@Override
		public void add(Mail bean, ITransaction transaction) throws PortalException
		{
			super.add(bean, transaction);
		}

		@Override
		public boolean delete(String id, ITransaction transaction) throws PortalException
		{
			return super.delete(id, transaction);
		}
	}
	
	private MailConfigDao mailConfigDao = new MailConfigDao();
	private MailDao mailDao = new MailDao();

	public IMultipleMail createMultipleMail() throws PortalException
	{
		return new Mail();
	}

	public ISingleMail createSingleMail() throws PortalException
	{
		return new Mail();
	}

	public IMailAttachment createMailAttachment()
	{
		return new MailAttachment();
	}

	public IMailDestination createMailDestination()
	{
		return new MailDestination();
	}
	
	public String generateNewQueueId() throws DaoException
	{
		return PortalUtil.generateNewGuid();
	}

	public IMailConfig getMailConfig(ITransaction transaction) throws DaoException
	{
		List<MailConfig> mailConfigs = mailConfigDao.findByCriteria(transaction, (Criterion[])null);
		if (!mailConfigs.isEmpty())
			return mailConfigs.get(0);
		else
			return new MailConfig();
	}

	public void setMailConfig(IMailConfig mailConfig, ITransaction transaction) throws DaoException
	{
		try
		{
			// add or update given mailConfig
			MailConfig newMailConfig = (MailConfig)mailConfig;
			mailConfigDao.addOrUpdate(newMailConfig, transaction);
	
			// delete any other existing mail config
			List<MailConfig> existingMailConfigs = mailConfigDao.findByCriteria(transaction, (Criterion[])null);
			if (existingMailConfigs.size() > 1)
			{
				for (MailConfig existingMailConfig: existingMailConfigs)
				{
					if (!existingMailConfig.getId().equals(newMailConfig.getId()))
						mailConfigDao.delete(existingMailConfig.getId(), transaction);
				}
				
			}
		}
		catch(PortalException e)
		{
			throw new DaoException(e.getMessage(), e);
		}
	}
	
	/**
	 * Persists the information about the mail, so it is queued and can be processed later.
	 * @param mail the mail.
	 * @param queueId the id of the queue where the mail will be added to. If null a new
	 * queue will be generated.
	 * @param transaction the transaction for persistence access.
	 * @throws DaoException if it was not possible to queue the mail.
	 * @since 4.0.4
	 */
	private void addMailToSendQueue(Mail mail, String queueId, ITransaction transaction) throws DaoException
	{
		try
		{
			Session session = ((ITransactionHibernate)transaction).getSession();

			// try to get the queue
			MailQueue mailQueue = null;
			if (queueId != null)
				mailQueue = (MailQueue)session.get(MailQueue.class, queueId, LockMode.READ);
			
			// if not found, create a new one
			if (mailQueue == null)
			{
				mailQueue = new MailQueue();
				if (queueId == null)
					queueId = generateNewQueueId();
				mailQueue.setId(queueId);
				mailQueue.setLocked(false);
				mailQueue.setRequestDateTime(new Date());
				session.persist(mailQueue);
			}

			// associate the mail with the queue and set its initial status
			mail.setMailQueue(mailQueue);
			mail.setRequestDateTime(new Date());
			mail.setStatus(IMailSendStatus.STATUS_SEND_PENDING);
			mail.setErrorMessage(null);
			mailDao.add(mail, transaction);
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e);
		}		
	}

	public void addMailToSendQueue(ISingleMail mail, String queueId, ITransaction transaction) throws DaoException
	{
		addMailToSendQueue((Mail)mail, queueId, transaction);
	}

	public void addMailToSendQueue(IMultipleMail mail, String queueId, ITransaction transaction) throws DaoException
	{
		addMailToSendQueue((Mail)mail, queueId, transaction);
	}

	/**
	 * This implementation actually uses the same mail instance, only sets the values in the
	 * to, cc and bcc fields.
	 * <p>
	 * This can be done because both {@link IMultipleMail} and {@link ISingleMail} are implemented 
	 * by the {@link Mail} class, but the {@link ISingleMail} destination fields are ignored when
	 * used as a {@link IMultipleMail} in this Dao.
	 * @param mail the mail.
	 * @param destination the destination
	 */
	public ISingleMail createSingleMail(IMultipleMail mail, IMailDestination destination)
	{
		ISingleMail singleMail = (ISingleMail)mail;

		// generate the to field value
		String toField;
		String name = destination.getName();
		if (name != null)
			toField = name + " <" + destination.getEmailAddress() + ">";
		else
			toField = destination.getEmailAddress();
		
		// fill the destination fields 
		singleMail.setTo(toField);
		singleMail.setCc(null);
		singleMail.setBcc(null);
		
		return singleMail;
	}

	public IBaseMail fetchNextQueuedMail(String queueId) throws DaoException
	{
		try
		{
			Mail mail = null;
			
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();
				
				Session session = transaction.getSession();

				// get the next mail to process, locked for upgrade
				Query query = session.createQuery("from Mail as m inner join fetch m.body " +
						"where m.mailQueue.id = :queueId and " +
						"(m.status = :pending or exists (from m.destinations as md where md.status = :pending)) " +
						"order by m.requestDateTime")
						.setParameter("queueId", queueId)
						.setInteger("pending", IMailSendStatus.STATUS_SEND_PENDING);
				ScrollableResults mails = query.scroll();
				try
				{
					if (mails.next())
					{
						mail = (Mail)mails.get(0);
						
						// force to load the attachment relationship
						mail.getDestinations().size();
						mail.getBody().getAttachments().size();
						
						// change the mail status to sending
						mail.setStatus(IMailSendStatus.STATUS_SENDING);
						
						// update the queue lock expiration date
						mail.getMailQueue().setLockExpirationDateTime(getNextQueueLockExpiration(transaction));
					}
				}
				finally
				{
					mails.close();
				}
				
				// commit the transaction
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
			
			return mail;
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}

	public void unfetchMail(String mailId) throws DaoException
	{
		try
		{		
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();
				
				// get the mail
				Session session = transaction.getSession();
				Mail mail = (Mail)session.load(Mail.class, mailId);
				mail.setStatus(IMailSendStatus.STATUS_SEND_PENDING);
				
				// commit the transaction
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}		
	}

	public IMailDestination fetchNextPendingDestination(IMultipleMail mail) throws DaoException
	{
		try
		{
			MailDestination destination = null;
			
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();
				
				// get the next destination to process, locked for upgrade
				Session session = transaction.getSession();
				Criteria criteria = session.createCriteria(MailDestination.class)
						.add(Restrictions.eq("mail", mail))
						.add(Restrictions.eq("status", IMailSendStatus.STATUS_SEND_PENDING));
				ScrollableResults destinations = criteria.scroll();
				try
				{
					if (destinations.next())
					{
						destination = (MailDestination)destinations.get(0);
						
						// change the destination status to sending
						destination.setStatus(IMailSendStatus.STATUS_SENDING);

						// update the queue lock expiration date
						destination.getMail().getMailQueue().setLockExpirationDateTime(getNextQueueLockExpiration(transaction));
					}
				}
				finally
				{
					destinations.close();
				}
				
				// commit the transaction
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
			
			return destination;
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}

	public void updateMailSendStatus(IMailSendStatus mailSendStatus) throws DaoException
	{
		try
		{
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();
				
				Session session = transaction.getSession();
				session.merge(mailSendStatus);
				
				// commit the transaction
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}
	
	/**
	 * Returns the date to be used as the next queue lock expiration date time.
	 * The date returned is calculated using the current system time and the
	 * configured queue lock timeout.
	 * @param transaction the transaction for persistence access.
	 * @return the generated next queue lock expiration. 
	 * @since 4.0.4
	 */
	private Date getNextQueueLockExpiration(ITransaction transaction) throws DaoException
	{
		int timeoutSeconds = getMailConfig(transaction).getQueueLockTimeout();
		return new Date(System.currentTimeMillis() + 1000L*timeoutSeconds);
	}

	public String lockNextQueue() throws DaoException
	{
		try
		{
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();
				
				// get the next queue to process, locked for upgrade
				String queueId = null;
				Session session = transaction.getSession();
				Criteria criteria = session.createCriteria(MailQueue.class)
						.setLockMode(LockMode.UPGRADE)
						.add(Restrictions.or(Restrictions.eq("locked", false), Restrictions.lt("lockExpirationDateTime", new Date())))
						.addOrder(Order.asc("requestDateTime"));
				ScrollableResults queues = criteria.scroll();
				try
				{
					if (queues.next())
					{
						MailQueue queue = (MailQueue)queues.get(0);
						
						// lock the queue and get its id
						queue.setLocked(true);
						queue.setLockExpirationDateTime(getNextQueueLockExpiration(transaction));	
						queueId = queue.getId();
					}
				}
				finally
				{
					queues.close();
				}
				
				// commit the transaction
				transaction.commit();
				
				// return the queue's id
				return queueId;
			}
			finally
			{
				transaction.dispose();
			}
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}

	public void unlockQueue(String queueId) throws DaoException
	{
		try
		{
			ITransactionHibernate transaction = (ITransactionHibernate)PortalTransactionFactory.createTransaction();
			try
			{
				// begin the transaction
				transaction.begin();

				Session session = transaction.getSession();
				MailQueue queue = (MailQueue)session.load(MailQueue.class, queueId);
				if (isQueuePending(queue, transaction))
				{
					// unlock the queue
					queue.setLocked(false);
				}
				else
				{
					// delete the queue
					session.delete(queue);
				}
				
				// commit the transaction
				transaction.commit();
			}
			finally
			{
				transaction.dispose();
			}
		}
		catch (PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}
	
	/**
	 * Checking if the queue has mail pending to be sent.
	 * @param queue the queue.
	 * @return true if the queue has mail pending to be sent, false otherwise.
	 * @since 4.0.4
	 */
	private boolean isQueuePending(MailQueue queue, ITransaction transaction) throws DaoException
	{
		ITransactionHibernate transactionHibernate = (ITransactionHibernate)transaction;
		Session session = transactionHibernate.getSession();
		
		// if this queue has any mail associated
		int mailCount = ((Number)session.createQuery("select count(*) from Mail as m where m.mailQueue = :queue and " +
				"(m.status = :pending or exists (from m.destinations as md where md.status = :pending))")
				.setParameter("queue", queue)
				.setInteger("pending", IMailSendStatus.STATUS_SEND_PENDING)
				.iterate().next()).intValue();
		return mailCount > 0;
	}

	public void deleteMail(String mailId, ITransaction transaction) throws DaoException
	{
		try
		{
			mailDao.delete(mailId, transaction);
		}
		catch(PortalException e)
		{
			throw new DaoException(e.getMessage(), e.getResources(), e);
		}
	}

	public void deleteMailBody(String mailId, ITransaction transaction) throws DaoException
	{
		ITransactionHibernate transactionHibernate = (ITransactionHibernate)transaction;
		Session session = transactionHibernate.getSession();
		
		Mail mail = (Mail)session.load(Mail.class, mailId);
		IMailBody body = mail.getBody();
		((Mail)mail).setBody(null);
		session.delete(body);
		session.flush();
	}
}